package com.moralesce.gt.math;

import static com.moralesce.gt.util.ListUtil.merge;
import static org.apache.commons.collections.CollectionUtils.containsAny;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections.CollectionUtils;

import com.moralesce.gt.model.Board;
import com.moralesce.gt.model.Card;
import com.moralesce.gt.model.Deck;
import com.moralesce.gt.model.Hand;
import com.moralesce.gt.model.Rank;
import com.moralesce.gt.model.StartingHand;
import com.moralesce.gt.util.ListUtil;

/**
 * Foundational enumerations.
 * 
 * <p>
 * To study probabilities and statistics in poker, common types of data sets can
 * be used used in Monte Carlo style simulations or direct computations.
 * </p>
 * 
 * <p>
 * This class makes the following data sets available:<br/>
 * - Every 5-card poker hand.<br/>
 * - Every 5-card poker hand that can be made with a specified starting hand.<br/>
 * - Every 5-card poker hand that can be made with a specified starting hand
 * after the flop.<br/>
 * - Every 5-card poker hand that can be made with a specified starting hand
 * after the turn.<br/>
 * - Every 5-card poker hand that can be made with a specified starting hand
 * after the river.<br/>
 * </p>
 * 
 * @author Brandon Grenier
 */
public final class Enumerations {

	private static List<Hand> all = null;
	private static List<Hand> dis = null;

	private Enumerations() {

	}

	/**
	 * Returns all possible 5-card poker hands.
	 * 
	 * <p>
	 * This method returns a list of all possible 5-card poker hands. There are
	 * (52c5) 2,598,960 5-card poker hands.
	 * </p>
	 * 
	 * @return A List<Hand> of all possible 5-card poker hands.
	 */
	public static List<Hand> allHands() {
		if (all != null) {
			return all;
		}
		all = toHands(new Combination(new Deck().getCards(), 5).list());
		return all;
	}

	/**
	 * Returns all possible 5-card poker hands that could be made with the given
	 * starting hand.
	 * 
	 * <p>
	 * This method assumes that a deck will contain exactly 50 cards after the
	 * specified starting hand has been dealt. The assumption guarantees that
	 * exactly 19,600 (50c3) 5-card poker hands will be returned for any given
	 * starting hand.
	 * </p>
	 * 
	 * @return A List<Hand> of all possible 5-card poker hands that can be made
	 * with the given starting hand.
	 */
	public static List<Hand> allHands(StartingHand startingHand) {
		return allHands(startingHand, new Deck());
	}

	/**
	 * Returns all possible 5-card poker hands that could be made with the given
	 * starting hand.
	 * 
	 * <p>
	 * This method does not assume that a deck will contain 50 cards after the
	 * starting hand has been dealt. This method will enumerate all 5-card hands
	 * that can be made with the cards that remain in the deck and the starting
	 * hand.
	 * </p>
	 * 
	 * @return A List<Hand> of all possible 5-card poker hands that can be made
	 * with the given starting hand.
	 */
	public static List<Hand> allHands(StartingHand startingHand, Deck deck) {
		deck.remove(startingHand.getCards());
		List<List<Card>> combinations = new Combination(deck.getCards(), 3).list();

		List<Hand> hands = new ArrayList<Hand>();
		for (List<Card> cards : combinations) {
			hands.add(Hand.newInstance(merge(cards, startingHand.getCards())));
		}
		return hands;
	}

	/**
	 * Returns all possible 5-card poker hands that could be made with the given
	 * starting hand and board.
	 * 
	 * <p>
	 * This method assumes that a deck will contain exactly 50 cards after the
	 * specified starting hand has been dealt. The deck will contain 47 cards
	 * after the flop, 46 cards after the turn and 45 cards after the river.
	 * </p>
	 * 
	 * <p>
	 * Once the community cards have been dealt, there are 21 (7c5) ways to
	 * choose a 5-card poker hand from the 5 community cards and 2 hole cards.
	 * The total number of hands that will be returned from this method are as
	 * follows:
	 * </p>
	 * 
	 * <p>
	 * If the board has 5 community cards, the flop, turn & river have been
	 * dealt:<br/>
	 * 21 hands -> 21 (7c5) * 1 (45c0)
	 * </p>
	 * 
	 * <p>
	 * If the board has 4 community cards, the flop and turn have been dealt:<br/>
	 * 966 hands -> 21 (7c5) * 46 (46c1)
	 * </p>
	 * 
	 * <p>
	 * If the board has 3 community cards, the flop has been dealt:<br/>
	 * 22701 hands -> 21 (7c5) * 1081 (47c2)
	 * </p>
	 * 
	 * <p>
	 * Because this method makes an assumption about the state of the deck
	 * before the enumeration process begins, it guarantees an exact number of
	 * 5-card poker hands will be returned for any given starting hand, and the
	 * state of the board.
	 * </p>
	 * 
	 * @return A List<Hand> of all possible 5-card poker hands that can be made
	 * with the given starting hand and board.
	 */
	public static List<Hand> allHands(StartingHand startingHand, Board board) {
		if (containsAny(startingHand.getCards(), board.getCards())) {
			throw new IllegalStateException("One or more cards belong to the starting hand and the board.");
		}
		Deck deck = new Deck().remove(ListUtil.merge(startingHand.getCards(), board.getCards()));
		return allHands(startingHand, board, deck);
	}

	/**
	 * Returns all possible 5-card poker hands that could be made with the given
	 * starting hand and board.
	 * 
	 * <p>
	 * This method does not assume that a deck will contain exactly 50 cards
	 * after the specified starting hand has been dealt. This method will
	 * enumerate all 5-card hands that can be made with the cards that remain in
	 * the deck, the starting hand and the board.
	 * </p>
	 * 
	 * @return A List<Hand> of all possible 5-card poker hands that can be made
	 * with the given starting hand and board.
	 */
	public static List<Hand> allHands(StartingHand startingHand, Board board, Deck deck) {
		if (containsAny(startingHand.getCards(), board.getCards())) {
			throw new IllegalStateException("One or more cards belong to the starting hand and the board.");
		}

		// If we have a full board, simply enumerate through what's available.
		if (board.getCards().size() == 5) {
			List<Card> spread = ListUtil.merge(startingHand.getCards(), board.getCards());
			return toHands(new Combination(spread, 5).list());
		}

		deck.remove(ListUtil.merge(startingHand.getCards(), board.getCards()));
		List<List<Card>> cards = new Combination(deck.getCards(), 5 - board.getCards().size()).list();

		List<Hand> hands = new ArrayList<Hand>();
		for (List<Card> top : cards) {
			List<Card> available = ListUtil.merge(startingHand.getCards(), board.getCards(), top);
			hands.addAll(toHands(new Combination(available, 5).list()));
		}
		return hands;
	}

	/**
	 * Returns all distinct 5-card poker hands.
	 * 
	 * @return A List<Hand> of distinct 5-card poker hands.
	 */
	public static List<Hand> distinctHands() {
		if (dis != null) {
			return dis;
		}

		Map<Integer, Hand> map = new HashMap<Integer, Hand>();
		for (Hand hand : allHands()) {
			map.put(hand.getKey(), hand);
		}
		dis = new ArrayList<Hand>(map.values());
		return dis;
	}

	/**
	 * Returns all distinct 5-card poker hands that could be made with the given
	 * starting hand.
	 * 
	 * @return A List<Hand> of distinct 5-card poker hands that can be made with
	 * the given starting hand.
	 */
	public static List<Hand> distinctHands(StartingHand startingHand) {
		Map<Integer, Hand> uniqueHands = new HashMap<Integer, Hand>();
		for (Hand hand : allHands(startingHand)) {
			uniqueHands.put(hand.getKey(), hand);
		}
		return new ArrayList<Hand>(uniqueHands.values());
	}

	/**
	 * Returns all distinct 5-card poker hands that could be made with the given
	 * board and starting hand.
	 * 
	 * @return A List<Hand> of distinct 5-card poker hands that can be made with
	 * the given starting hand.
	 */
	public static List<Hand> distinctHands(StartingHand startingHand, Board board) {
		Map<Integer, Hand> map = new HashMap<Integer, Hand>();
		for (Hand hand : allHands(startingHand, board)) {
			map.put(hand.getKey(), hand);
		}
		return new ArrayList<Hand>(map.values());
	}

	/**
	 * Returns all 5-card poker hands that can be made with the given board and
	 * starting hand.
	 * 
	 * <p>
	 * This method returns all 5 card poker hands that can be made with the
	 * given starting hand and board, but excludes 'high card' hands and hands
	 * that are made up entirely of community cards.
	 * </p>
	 * 
	 * <p>
	 * This method assumes that a deck will contain exactly 50 cards after the
	 * specified starting hand has been dealt.
	 * </p>
	 * 
	 * @return A List<Hand> of distinct 5-card poker hands that can be made with
	 * the given starting hand.
	 */
	public static List<Hand> exclusiveHands(StartingHand startingHand, Board board) {
		return exclusiveHands(startingHand, board, new Deck());
	}

	/**
	 * Returns all 5-card poker hands that can be made with the given board and
	 * starting hand.
	 * 
	 * <p>
	 * This method returns all 5 card poker hands that can be made with the
	 * given starting hand and board, but excludes 'high card' hands and hands
	 * that are made up entirely of community cards.
	 * </p>
	 * 
	 * <p>
	 * This method does not assume that a deck will contain exactly 50 cards
	 * after the specified starting hand has been dealt. This method will
	 * enumerate all 5-card hands that can be made with the cards that remain in
	 * the deck, the starting hand and the board.
	 * </p>
	 * 
	 * @return A List<Hand> of distinct 5-card poker hands that can be made with
	 * the given starting hand.
	 */
	public static List<Hand> exclusiveHands(StartingHand startingHand, Board board, Deck deck) {
		List<Hand> hands = allHands(startingHand, board, deck);
		List<Hand> exclusiveHands = new ArrayList<Hand>();

		for (Hand hand : hands) {
			if (hand.hasStraightFlush() && handContainsAPocketCard(hand, startingHand)) {
				exclusiveHands.add(hand);
			}
			if (hand.hasFlush() && handContainsAPocketCard(hand, startingHand)) {
				exclusiveHands.add(hand);
			}
			if (hand.hasStraight() && handContainsAPocketCard(hand, startingHand)) {
				exclusiveHands.add(hand);
			}
			if (hand.hasFullHouse() && handContainsAPocketCard(hand, startingHand)) {
				exclusiveHands.add(hand);
			}
			if (hand.hasFourOfAKind() && handContainsAPocketCard(hand, startingHand)) {
				exclusiveHands.add(hand);
			}
			if (hand.hasThreeOfAKind()) {
				if (!hasThreeOfAKind(communityCards(startingHand, hand))) {
					exclusiveHands.add(hand);
				}
			}
			if (hand.hasTwoPair() && handContainsAPocketCard(hand, startingHand)) {
				exclusiveHands.add(hand);
			}
			if (hand.hasPair()) {
				if (!hasPair(communityCards(startingHand, hand))) {
					exclusiveHands.add(hand);
				}
			}
		}

		return ListUtil.sort(exclusiveHands);
	}

	public static List<Hand> toHands(List<List<Card>> cards) {
		List<Hand> hands = new ArrayList<Hand>();
		for (List<Card> instance : cards) {
			hands.add(Hand.newInstance(instance));
		}
		return hands;
	}

	private static boolean handContainsAPocketCard(Hand hand, StartingHand startingHand) {
		return CollectionUtils.containsAny(hand.getCards(), startingHand.getCards());
	}

	private static List<Card> communityCards(StartingHand startingHand, Hand hand) {
		List<Card> cards = ListUtil.copy(hand.getCards());
		cards.removeAll(startingHand.getCards());
		return cards;
	}

	private static boolean hasPair(List<Card> cards) {
		for (Rank rank : Rank.values()) {
			if (count(rank, cards) == 2) {
				return true;
			}
		}
		return false;
	}

	private static boolean hasThreeOfAKind(List<Card> cards) {
		for (Rank rank : Rank.values()) {
			if (count(rank, cards) == 3) {
				return true;
			}
		}
		return false;
	}

	private static Integer count(Rank rank, List<Card> cards) {
		int total = 0;
		for (Card card : cards) {
			if (card.getRank().equals(rank)) {
				total += 1;
			}
		}
		return total;
	}

}
